/*
 * Title:        CloudSim Toolkit
 * Description:  CloudSim (Cloud Simulation) Toolkit for Modeling and Simulation of Clouds
 * Licence:      GPL - http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2009-2012, The University of Melbourne, Australia
 */

package cloudsim;

import cloudsim.core.CloudSim;

/**
 * CloudSim ResCloudlet represents a Cloudlet submitted to CloudResource for processing. This class
 * keeps track the time for all activities in the CloudResource for a specific Cloudlet. Before a
 * Cloudlet exits the CloudResource, it is RECOMMENDED to call this method
 * {@link #finalizeCloudlet()}.
 * <p/>
 * It contains a Cloudlet object along with its arrival time and the ID of the machine and the Pe
 * (Processing Element) allocated to it. It acts as a placeholder for maintaining the amount of
 * resource share allocated at various times for simulating any scheduling using internal events.
 *
 * @author Manzur Murshed
 * @author Rajkumar Buyya
 * @since CloudSim Toolkit 1.0
 */
public class ResCloudlet {

    /**
     * The Cloudlet object.
     */
    private final Cloudlet cloudlet;

    /**
     * The Cloudlet arrival time for the first time.
     */
    private double arrivalTime;

    /**
     * The estimation of Cloudlet finished time.
     */
    private double finishedTime;

    /**
     * The length of Cloudlet finished so far.
     */
    private long cloudletFinishedSoFar;

    /**
     * Cloudlet execution start time. This attribute will only hold the latest time since a Cloudlet
     * can be canceled, paused or resumed.
     */
    private double startExecTime;

    /**
     * The total time to complete this Cloudlet.
     */
    private double totalCompletionTime;

    // The below attributes are only to be used by the SpaceShared policy.

    /**
     * The machine id this Cloudlet is assigned to.
     */
    private int machineId;

    /**
     * The Pe id this Cloudlet is assigned to.
     */
    private int peId;

    /**
     * The an array of machine IDs.
     */
    private int[] machineArrayId = null;

    /**
     * The an array of Pe IDs.
     */
    private int[] peArrayId = null;

    /**
     * The index of machine and Pe arrays.
     */
    private int index;

    // NOTE: Below attributes are related to AR stuff

    /**
     * The Constant NOT_FOUND.
     */
    private static final int NOT_FOUND = -1;

    /**
     * The reservation start time.
     */
    private final long startTime;

    /**
     * The reservation duration time.
     */
    private final int duration;

    /**
     * The reservation id.
     */
    private final int reservId;

    /**
     * The num Pe needed to execute this Cloudlet.
     */
    private int pesNumber;

    /**
     * Allocates a new ResCloudlet object upon the arrival of a Cloudlet object. The arriving time
     * is determined by {@link gridsim.CloudSim#clock()}.
     *
     * @param cloudlet a cloudlet object
     * @pre cloudlet != null
     * @post $none
     * @see gridsim.CloudSim#clock()
     */
    public ResCloudlet(Cloudlet cloudlet) {
        // when a new ResCloudlet is created, then it will automatically set
        // the submission time and other properties, such as remaining length
        this.cloudlet = cloudlet;
        startTime = 0;
        reservId = NOT_FOUND;
        duration = 0;

        init();
    }

    /**
     * Allocates a new ResCloudlet object upon the arrival of a Cloudlet object. Use this
     * constructor to store reserved Cloudlets, i.e. Cloudlets that done reservation before. The
     * arriving time is determined by {@link gridsim.CloudSim#clock()}.
     *
     * @param cloudlet  a cloudlet object
     * @param startTime a reservation start time. Can also be interpreted as starting time to
     *                  execute this Cloudlet.
     * @param duration  a reservation duration time. Can also be interpreted as how long to execute
     *                  this Cloudlet.
     * @param reservID  a reservation ID that owns this Cloudlet
     * @pre cloudlet != null
     * @pre startTime > 0
     * @pre duration > 0
     * @pre reservID > 0
     * @post $none
     * @see gridsim.CloudSim#clock()
     */
    public ResCloudlet(Cloudlet cloudlet, long startTime, int duration, int reservID) {
        this.cloudlet = cloudlet;
        this.startTime = startTime;
        reservId = reservID;
        this.duration = duration;

        init();
    }

    /**
     * Gets the Cloudlet or reservation start time.
     *
     * @return Cloudlet's starting time
     * @pre $none
     * @post $none
     */
    public long getStartTime() {
        return startTime;
    }

    /**
     * Gets the reservation duration time.
     *
     * @return reservation duration time
     * @pre $none
     * @post $none
     */
    public int getDurationTime() {
        return duration;
    }

    /**
     * Gets the number of PEs required to execute this Cloudlet.
     *
     * @return number of Pe
     * @pre $none
     * @post $none
     */
    public int getNumberOfPes() {
        return pesNumber;
    }

    /**
     * Gets the reservation ID that owns this Cloudlet.
     *
     * @return a reservation ID
     * @pre $none
     * @post $none
     */
    public int getReservationID() {
        return reservId;
    }

    /**
     * Checks whether this Cloudlet is submitted by reserving or not.
     *
     * @return <tt>true</tt> if this Cloudlet has reserved before, <tt>false</tt> otherwise
     * @pre $none
     * @post $none
     */
    public boolean hasReserved() {
        if (reservId == NOT_FOUND) {
            return false;
        }

        return true;
    }

    /**
     * Initialises all local attributes.
     *
     * @pre $none
     * @post $none
     */
    private void init() {
        // get number of PEs required to run this Cloudlet
        pesNumber = cloudlet.getNumberOfPes();

        // if more than 1 Pe, then create an array
        if (pesNumber > 1) {
            machineArrayId = new int[pesNumber];
            peArrayId = new int[pesNumber];
        }

        arrivalTime = CloudSim.clock();
        cloudlet.setSubmissionTime(arrivalTime);

        // default values
        finishedTime = NOT_FOUND;  // Cannot finish in this hourly slot.
        machineId = NOT_FOUND;
        peId = NOT_FOUND;
        index = 0;
        totalCompletionTime = 0.0;
        startExecTime = 0.0;

        // In case a Cloudlet has been executed partially by some other grid
        // hostList.
        cloudletFinishedSoFar = cloudlet.getCloudletFinishedSoFar() * Consts.MILLION;
    }

    /**
     * Gets this Cloudlet entity Id.
     *
     * @return the Cloudlet entity Id
     * @pre $none
     * @post $none
     */
    public int getCloudletId() {
        return cloudlet.getCloudletId();
    }

    /**
     * Gets the user or owner of this Cloudlet.
     *
     * @return the Cloudlet's user Id
     * @pre $none
     * @post $none
     */
    public int getUserId() {
        return cloudlet.getUserId();
    }

    /**
     * Gets the Cloudlet's length.
     *
     * @return Cloudlet's length
     * @pre $none
     * @post $none
     */
    public long getCloudletLength() {
        return cloudlet.getCloudletLength();
    }

    /**
     * Gets the total Cloudlet's length (across all PEs).
     *
     * @return total Cloudlet's length
     * @pre $none
     * @post $none
     */
    public long getCloudletTotalLength() {
        return cloudlet.getCloudletTotalLength();
    }

    /**
     * Gets the Cloudlet's class type.
     *
     * @return class type of the Cloudlet
     * @pre $none
     * @post $none
     */
    public int getCloudletClassType() {
        return cloudlet.getClassType();
    }

    /**
     * Sets the Cloudlet status.
     *
     * @param status the Cloudlet status
     * @return <tt>true</tt> if the new status has been set, <tt>false</tt> otherwise
     * @pre status >= 0
     * @post $none
     */
    public boolean setCloudletStatus(int status) {
        // gets Cloudlet's previous status
        int prevStatus = cloudlet.getStatus();

        // if the status of a Cloudlet is the same as last time, then ignore
        if (prevStatus == status) {
            return false;
        }

        boolean success = true;
        try {
            double clock = CloudSim.clock();   // gets the current clock

            // sets Cloudlet's current status
            cloudlet.setCloudletStatus(status);

            // if a previous Cloudlet status is INEXEC
            if (prevStatus == Cloudlet.INEXEC) {
                // and current status is either CANCELED, PAUSED or SUCCESS
                if (status == Cloudlet.CANCELED || status == Cloudlet.PAUSED || status == Cloudlet.SUCCESS) {
                    // then update the Cloudlet completion time
                    totalCompletionTime += (clock - startExecTime);
                    index = 0;
                    return true;
                }
            }

            if (prevStatus == Cloudlet.RESUMED && status == Cloudlet.SUCCESS) {
                // then update the Cloudlet completion time
                totalCompletionTime += (clock - startExecTime);
                return true;
            }

            // if a Cloudlet is now in execution
            if (status == Cloudlet.INEXEC || (prevStatus == Cloudlet.PAUSED && status == Cloudlet.RESUMED)) {
                startExecTime = clock;
                cloudlet.setExecStartTime(startExecTime);
            }

        } catch (Exception e) {
            success = false;
        }

        return success;
    }

    /**
     * Gets the Cloudlet's execution start time.
     *
     * @return Cloudlet's execution start time
     * @pre $none
     * @post $none
     */
    public double getExecStartTime() {
        return cloudlet.getExecStartTime();
    }

    /**
     * Sets this Cloudlet's execution parameters. These parameters are set by the CloudResource
     * before departure or sending back to the original Cloudlet's owner.
     *
     * @param wallClockTime the time of this Cloudlet resides in a CloudResource (from arrival time
     *                      until departure time).
     * @param actualCPUTime the total execution time of this Cloudlet in a CloudResource.
     * @pre wallClockTime >= 0.0
     * @pre actualCPUTime >= 0.0
     * @post $none
     */
    public void setExecParam(double wallClockTime, double actualCPUTime) {
        cloudlet.setExecParam(wallClockTime, actualCPUTime);
    }

    /**
     * Sets the machine and Pe (Processing Element) ID.
     *
     * @param machineId machine ID
     * @param peId      Pe ID
     * @pre machineID >= 0
     * @pre peID >= 0
     * @post $none
     * @todo the machineId param and attribute mean a VM or a PM id?
     * Only the term machine is ambiguous.
     * At {@link  CloudletSchedulerTimeShared#cloudletSubmit(Cloudlet)}
     * it is stated it is a VM.
     */
    public void setMachineAndPeId(int machineId, int peId) {
        // if this job only requires 1 Pe
        this.machineId = machineId;
        this.peId = peId;

        // if this job requires many PEs
        if (peArrayId != null && pesNumber > 1) {
            machineArrayId[index] = machineId;
            peArrayId[index] = peId;
            index++;
        }
    }

    /**
     * Gets machine ID.
     *
     * @return machine ID or <tt>-1</tt> if it is not specified before
     * @pre $none
     * @post $result >= -1
     */
    public int getMachineId() {
        return machineId;
    }

    /**
     * Gets Pe ID.
     *
     * @return Pe ID or <tt>-1</tt> if it is not specified before
     * @pre $none
     * @post $result >= -1
     */
    public int getPeId() {
        return peId;
    }

    /**
     * Gets a list of Pe IDs. <br>
     * NOTE: To get the machine IDs corresponding to these Pe IDs, use {@link #getMachineIdList()}.
     *
     * @return an array containing Pe IDs.
     * @pre $none
     * @post $none
     */
    public int[] getPeIdList() {
        return peArrayId;
    }

    /**
     * Gets a list of Machine IDs. <br>
     * NOTE: To get the Pe IDs corresponding to these machine IDs, use {@link #getPeIdList()}.
     *
     * @return an array containing Machine IDs.
     * @pre $none
     * @post $none
     */
    public int[] getMachineIdList() {
        return machineArrayId;
    }

    /**
     * Gets the remaining cloudlet length that has to be execute yet,
     * considering the {@link #getCloudletTotalLength()}.
     *
     * @return cloudlet length
     * @pre $none
     * @post $result >= 0
     */
    public long getRemainingCloudletLength() {
        long length = cloudlet.getCloudletTotalLength() * Consts.MILLION - cloudletFinishedSoFar;

        // Remaining Cloudlet length can't be negative number.
        if (length < 0) {
            return 0;
    }

        return (long) Math.floor(length / Consts.MILLION);
    }

    /**
     * Finalizes all relevant information before <tt>exiting</tt> the CloudResource entity. This
     * method sets the final data of:
     * <ul>
     * <li>wall clock time, i.e. the time of this Cloudlet resides in a CloudResource (from arrival
     * time until departure time).
     * <li>actual CPU time, i.e. the total execution time of this Cloudlet in a CloudResource.
     * <li>Cloudlet's finished time so far
     * </ul>
     *
     * @pre $none
     * @post $none
     */
    public void finalizeCloudlet() {
        // Sets the wall clock time and actual CPU time
        double wallClockTime = CloudSim.clock() - arrivalTime;
        cloudlet.setExecParam(wallClockTime, totalCompletionTime);

        long finished = 0;
        //if (cloudlet.getCloudletTotalLength() * Consts.MILLION < cloudletFinishedSoFar) {
        if (cloudlet.getStatus() == Cloudlet.SUCCESS) {
            finished = cloudlet.getCloudletLength();
        } else {
            finished = cloudletFinishedSoFar / Consts.MILLION;
        }

        cloudlet.setCloudletFinishedSoFar(finished);
    }

    /**
     * Updates the length of cloudlet that has already been completed.
     *
     * @param miLength cloudlet length in Instructions (I)
     * @pre miLength >= 0.0
     * @post $none
     */
    public void updateCloudletFinishedSoFar(long miLength) {
        cloudletFinishedSoFar += miLength;
    }

    /**
     * Gets arrival time of a cloudlet.
     *
     * @return arrival time
     * @pre $none
     * @post $result >= 0.0
     * @todo It is being used different words for the same term.
     * Here it is used arrival time while at Resource inner classe of the Cloudlet class
     * it is being used submissionTime. It needs to be checked if they are
     * the same term or different ones in fact.
     */
    public double getCloudletArrivalTime() {
        return arrivalTime;
    }

    /**
     * Sets the finish time for this Cloudlet. If time is negative, then it is being ignored.
     *
     * @param time finish time
     * @pre time >= 0.0
     * @post $none
     */
    public void setFinishTime(double time) {
        if (time < 0.0) {
            return;
        }

        finishedTime = time;
    }

    /**
     * Gets the Cloudlet's finish time.
     *
     * @return finish time of a cloudlet or <tt>-1.0</tt> if it cannot finish in this hourly slot
     * @pre $none
     * @post $result >= -1.0
     */
    public double getClouddletFinishTime() {
        return finishedTime;
    }

    /**
     * Gets the related Cloudlet object.
     *
     * @return cloudlet object
     * @pre $none
     * @post $result != null
     */
    public Cloudlet getCloudlet() {
        return cloudlet;
    }

    /**
     * Gets the Cloudlet status.
     *
     * @return Cloudlet status
     * @pre $none
     * @post $none
     */
    public int getCloudletStatus() {
        return cloudlet.getStatus();
    }

    /**
     * Get am Unique Identifier (UID) of the cloudlet.
     *
     * @return The UID
     */
    public String getUid() {
        return getUserId() + "-" + getCloudletId();
    }

}
